package butterknife;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.ability.AbilitySlice;
import ohos.agp.components.Component;
import ohos.agp.window.dialog.CommonDialog;

import org.jetbrains.annotations.NotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * Field and method binding for openharmony Components. Use this class to simplify finding components and
 * attaching listeners by binding them with annotations.
 * <p>
 * Finding components from your ability is as easy as:
 * <pre><code>
 * public class ExampleAbilitySlice extends AbilitySlice {
 *   {@literal @}BindComponent(ResourceTable.Id_title) Text titleComponent;
 *   {@literal @}BindComponent(ResourceTable.Id_subtitle) Text subtitleComponent;
 *
 *   {@literal @}Override public void onStart(Intent intent) {
 *     super.onStart(intent);
 *     super.setUIContent(mRootLayout);
 *     ButterKnife.bind(this);
 *   }
 * }
 * </code></pre>
 * Binding can be performed directly on an { #bind(Ability) ability}, a
 * { #bind(Component) component}, or a { #bind(CommonDialog) dialog}. Alternate objects to
 * bind can be specified along with an { #bind(Object, Ability) ability},
 * { #bind(Object, Component) component}, or
 * { #bind(Object, ohos.agp.window.dialog.CommonDialog) dialog}.
 * <p>
 * Group multiple components together into a {@link List} or array.
 * <pre><code>
 * {@literal @}BindComponent({ResourceTable.Id_first_name, ResourceTable.Id_middle_name, ResourceTable.Id_last_name})
 * List<EditText> nameViews;
 * </code></pre>
 * <p>
 * To bind listeners to your components you can annotate your methods:
 * <pre><code>
 * {@literal @}OnClick(ResourceTable.Id_submit) void onSubmit() {
 *   // React to button click.
 * }
 * </code></pre>
 * Any number of parameters from the listener may be used on the method.
 * <pre><code>
 * {@literal @}OnItemClick(ResourceTable.Id_tweet_list) void onTweetClicked(int position) {
 *   // React to tweet click.
 * }
 * </code></pre>
 * <p>
 * Be default, components are required to be present in the layout for both field and method bindings.
 * If a component is optional add a {@code @Nullable} annotation for fields (such as the one in the
 * library)
 * or the {@code @Optional} annotation for methods.
 * <pre><code>
 * {@literal @}Nullable @BindComponent(ResourceTable.Id_title) Text subtitleView;
 * </code></pre>
 * Resources can also be bound to fields to simplify programmatically working with components:
 * <pre><code>
 * {@literal @}BindBool(ResourceTable.Boolean_is_tablet) boolean isTablet;
 * {@literal @}BindInt(ResourceTable.Integer_columns) int columns;
 * {@literal @}BindColor(ResourceTable.Color_error_red) int errorRed;
 * </code></pre>
 */
public final class ButterKnife {
    static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
    private static final String TAG = "ButterKnife";
    private static boolean debug = false;
    
    private static Field uiContent_Ability;
    private static Field uiContent_Slice;
    private static Field curComponentContainer;
    static {
        try {
            uiContent_Ability = Ability.class.getDeclaredField("uiContent");
            uiContent_Ability.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        try {
            uiContent_Slice = AbilitySlice.class.getDeclaredField("uiContent");
            uiContent_Slice.setAccessible(true);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        try {
            curComponentContainer = Class.forName("ohos.aafwk.ability.UIContent").getDeclaredField("curComponentContainer");
            curComponentContainer.setAccessible(true);
        } catch (NoSuchFieldException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private ButterKnife() {
        throw new AssertionError("No instances.");
    }

    /**
     * Control whether debug logging is enabled.
     * @param debug set debug parameter
     */
    public static void setDebug(boolean debug) {
        ButterKnife.debug = debug;
    }

    /**
     * BindComponent annotated fields and methods in the specified { Ability}. The current content
     * component is used as the component root.
     *
     * @param target Target ability for component binding.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull Ability target) {
        //Optional<Component> sourceComponent = target.getWindow().getCurrentComponentFocus();// is empty
        Optional<Component> sourceComponent = getCurComponentContainer(target);
        if (sourceComponent.isPresent())
            return bind(target, sourceComponent.get());
        else
            return null;
    }

    @NotNull
    public static Unbinder bind(@NotNull AbilitySlice target) {
        Optional<Component> sourceComponent = getCurComponentContainer(target);
        if (sourceComponent.isPresent())
            return bind(target, sourceComponent.get());
        else
            return null;
    }

    /**
     * BindComponent annotated fields and methods in the specified {Component}. The component and its children
     * are used as the component root.
     * @param target Target component for component binding.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull Component target) {
        return bind(target, target);
    }

    /**
     * BindComponent annotated fields and methods in the specified { CommonDialog}. The current content
     * component is used as the component root.
     *
     * @param target Target dialog for component binding.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull CommonDialog target) {
        Optional<Component> sourceComponent = target.getWindow().getCurrentComponentFocus();
        if (sourceComponent.isPresent())
            return bind(target, sourceComponent.get());
        else
            return null;
    }

    /**
     * BindComponent annotated fields and methods in the specified {@code target} using the {@code source}
     * { Ability} as the component root.
     *
     * @param target Target class for component binding.
     * @param source Ability on which IDs will be looked up.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull Object target, @NotNull Ability source) {
        Optional<Component> sourceComponent = source.getWindow().getCurrentComponentFocus();
        if (sourceComponent.isPresent())
            return bind(target, sourceComponent.get());
        else
            return null;
    }

    /**
     * BindComponent annotated fields and methods in the specified {@code target} using the {@code source}
     * { CommonDialog} as the component root.
     *
     * @param target Target class for component binding.
     * @param source CommonDialog on which IDs will be looked up.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull Object target, @NotNull CommonDialog source) {
        Optional<Component> sourceComponent = source.getWindow().getCurrentComponentFocus();
        if (sourceComponent.isPresent())
            return bind(target, sourceComponent.get());
        else
            return null;
    }

    /**
     * BindComponent annotated fields and methods in the specified {@code target} using the {@code source}
     * { Component} as the component root.
     *
     * @param target Target class for component binding.
     * @param source Component root on which IDs will be looked up.
     * @return bind return the unbinder
     */
	@NotNull
    public static Unbinder bind(@NotNull Object target, @NotNull Component source) {
        Class<?> targetClass = target.getClass();
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

        if (constructor == null) {
            return Unbinder.EMPTY;
        }

        // noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
        try {
            return constructor.newInstance(target, source);
        } catch (IllegalAccessException | InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }

	@NotNull
    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null || BINDINGS.containsKey(cls)) {
            return bindingCtor;
        }
        String clsName = cls.getName();
        if (clsName.startsWith("ohos.") || clsName.startsWith("java.")) {
            return null;
        }
        try {
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
            // noinspection unchecked
            bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, Component.class);
        } catch (ClassNotFoundException e) {
            bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        BINDINGS.put(cls, bindingCtor);
        return bindingCtor;
    }

    private static Optional<Component> getCurComponentContainer(Ability ability) {
        if (uiContent_Ability != null && curComponentContainer != null) {
            try {
                Object uiContentObj = uiContent_Ability.get(ability);
                Object curComponentContainerObj = curComponentContainer.get(uiContentObj);
                return Optional.ofNullable((Component) curComponentContainerObj);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return Optional.empty();
    }

    private static Optional<Component> getCurComponentContainer(AbilitySlice abilitySlice) {
        if (uiContent_Slice != null && curComponentContainer != null) {
            try {
                Object uiContentObj = uiContent_Slice.get(abilitySlice);
                Object curComponentContainerObj = curComponentContainer.get(uiContentObj);
                return Optional.ofNullable((Component) curComponentContainerObj);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return Optional.empty();
    }
}
